/*
 * This file (AkArtspace.java) is protected by the GNU GPL licence (v2). 
 * Please read the license.txt attached for further informations. 
 * 
 */

package alkwarel.objects;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;

import javax.swing.JOptionPane;

import processing.core.PApplet;
import processing.core.PImage;
import alkwarel.colors.AkColorManager;
import alkwarel.colors.AkColorMngrStandard;
import alkwarel.utils.AkConstants;
import artx.objects.AxArtspace;
import artx.objects.AxManager;
import artx.objects.AxParameterManager;
import artx.parameters.AxBoolParameter;
import artx.parameters.AxParameter;
import artx.utils.AxLog;
import artx.utils.AxRandom;

/**
 * An Artspace in Alkwarel is a Processing Drawing Frame (PApplet). To use this
 * in a GUI application, use the following code :
 * 
 * <pre>
 * JPanel content = new JPanel();
 * content.setLayout(new BorderLayout());
 * artspace = new AkApplet();
 * artspace.init();
 * content.add(artspace, BorderLayout.CENTER);
 * </pre>
 * 
 * For more info on Processing, please visit <a
 * href="http://www.processing.org/">http://www.processing.org/</a>.
 * 
 * @author Xavier Gouchet
 * 
 */
public class AkArtspace extends PApplet implements AxArtspace, AkConstants {

	private static final long serialVersionUID = 1L;

	/** Manager containing available Shapes. */
	public AxManager shapeManager = null;

	/** Manager containing available Filters. */
	public AxManager filterManager = null;

	/** Manager containing available Brushes. */
	public AxManager brushManager = null;

	/** Manager containing available Blenders. */
	public AxManager blenderManager = null;

	/** the manager of the artspace parameters */
	protected AxParameterManager paramsManager = null;

	/** Does the artspace works with a gui? */
	public boolean useGui = false;

	// TODO add a way to save the paramsManager to xml when new params are added

	private AkColorManager colorManager = null;

	private ArrayList<String> shapesInstructions = null;
	private ArrayList<String> filtersInstructions = null;

	private long id;

	private boolean state = false;
	private boolean reset = false;
	private boolean brushReset = false;
	private boolean brushEnabled = false;
	private String currentBrush = "default";
	private String currentBlend = "default";
	private PImage low = null;
	private PImage high = null;
	private int oldX, oldY;

	/**
	 * Standard constructor.
	 * 
	 */
	public AkArtspace() {
		AxLog.Debug("AkArtspace::AkArtspace =>> Creating new artspace");

		// alkwarel Init
		this.colorManager = new AkColorMngrStandard();
		this.paramsManager = new AxParameterManager(DIR_CONFIG + "artspace.xml");
		this.shapeManager = new AxManager(DIR_CONFIG + "shapes.list",
				AkShape.class, "alkwarel.shapes");
		this.filterManager = new AxManager(DIR_CONFIG + "filters.list",
				AkFilter.class, "alkwarel.filters");
		this.brushManager = new AxManager(DIR_CONFIG + "brushes.list", AkBrush.class,
				"alkwarel.brushes");
		this.blenderManager = new AxManager(DIR_CONFIG + "blenders.list",
				AkBlender.class, "alkwarel.blenders");

		this.Reset();
	}

	/**
	 * @see processing.core.PApplet#setup()
	 */
	public void setup() {
		size(640, 480);
		background(0);
		noLoop();

	}

	/**
	 * @see processing.core.PApplet#draw()
	 */
	public void draw() {
		if (this.reset) {
			AxLog.Debug("AkArtspace::draw =>> Resetting current artspace");
			if (this.g != null)
				if (this.g.pixels != null)
					Arrays.fill(this.g.pixels, 0);
			this.background(0);
			this.reset = false;
		}
		// update parameters
		this.UpdateParameters();

		// apply the brush
		// TODO handle blending for brushes too
		if (this.brushEnabled) {
			if (this.brushReset) {
				this.oldX = this.mouseX;
				this.oldY = this.mouseY;
				this.brushReset = false;
			}
			this.DrawStroke(oldX, oldY, mouseX, mouseY);
			this.oldX = this.mouseX;
			this.oldY = this.mouseY;
		} else {
			// apply instructions if any
			this.ApplyShapesInstructions();
			this.ApplyFiltersInstructions();
		}

		// Save current step
		if (!this.brushEnabled) {
			AxLog.Debug("AkArtspace::draw =>> Saving current artspace state");
			this.saveFrame("temp/artspace_" + this.id + ".###.tif");
		}
	}

	/**
	 * @see processing.core.PApplet#mousePressed()
	 */
	public void mousePressed() {
		// do something based on mouse movement
		AxLog.Debug("AkArtspace::mousePressed =>> starting brush strokes");
		this.brushEnabled = true;
		this.brushReset = true;
		this.state = true;
		this.ChangeColors();
		this.frameRate(30);
		this.loop();
	}

	/**
	 * @see processing.core.PApplet#mouseReleased()
	 */
	public void mouseReleased() {
		// do something based on mouse movement
		AxLog.Debug("AkArtspace::mouseReleased =>> stoping brush strokes");
		this.brushEnabled = false;
		this.noLoop();
		AxLog
				.Debug("AkArtspace::mouseReleased =>> Saving current artspace state");
		this.saveFrame("temp/artspace_" + this.id + ".###.tif");
	}

	private void ApplyShapesInstructions() {
		if ((this.shapesInstructions != null)
				&& (this.shapesInstructions.size() > 0)) {
			while (this.shapesInstructions.size() > 0) {
				String cmd = this.shapesInstructions.remove(0);
				AxLog.Debug("AkArtspace::ApplyShapesInstructions =>> "
						+ "Applying shape instruction " + cmd);

				if (this.shapeManager.Exists(cmd)) {
					AkShape shape = (AkShape) this.shapeManager.GetElement(cmd);
					if (shape != null) {
						// reset artspace state from params
						this.UpdateParameters();
						this.ChangeColors();
						// save current artspace state in a layer
						this.low = this.get();
						Arrays.fill(this.g.pixels, 0);
						// apply shape
						shape.Apply((AxArtspace) this);
						this.state = true;
						// merge layers
						this.high = this.get();
						this.Merge(this.low, this.high);
						continue;
					}
				}
				AxLog.Debug("AkArtspace::ApplyShapesInstructions =>> "
						+ "the instruction \"" + cmd + "\" does not exist");
			}
		}
	}

	private void ApplyFiltersInstructions() {
		if ((this.filtersInstructions != null)
				&& (this.filtersInstructions.size() > 0)) {
			while (this.filtersInstructions.size() > 0) {
				String cmd = this.filtersInstructions.remove(0);
				AxLog.Debug("AkArtspace::ApplyFiltersInstructions =>> "
						+ "Applying filter instruction " + cmd);

				if (this.filterManager.Exists(cmd)) {
					AkFilter filter = (AkFilter) this.filterManager
							.GetElement(cmd);
					if (filter != null) {
						filter.Apply((AxArtspace) this);
						this.state = true;
						continue;
					}
				}
				AxLog.Debug("AkArtspace::ApplyFiltersInstructions =>> "
						+ "the instruction \"" + cmd + "\" does not exist");
			}
		}
	}

	private void Merge(PImage low, PImage high) {
		AxLog.Debug("AkArtspace::Merge =>> merging layers");
		if (this.blenderManager.Exists("SimpleBlend")) {
			AkBlender blender = (AkBlender) this.blenderManager
					.GetElement(this.currentBlend);
			if (blender != null) {
				blender.Merge(this, low, high);
			}
		}
	}

	private void UpdateParameters() {
		// Smooth
		AxBoolParameter smoothParam = (AxBoolParameter) this.paramsManager
				.GetElement("smooth");
		if (smoothParam != null) {
			if (smoothParam.GetValue()) {
				this.smooth();
			} else {
				this.noSmooth();
			}
		}
	}

	/**
	 * @see artx.objects.AxArtspace#SaveArtspace(java.lang.String)
	 */
	public void SaveArtspace(String path) {
		AxLog.Debug("AkArtspace::SaveArtspace =>> Saving artspace history");
		// TODO save artspace history as text file
	}

	/**
	 * @param path
	 */
	public void ExportArtspace(String path) {
		AxLog.Debug("AkArtspace::SaveArtspace =>> Save artspace as image");
		this.save(path);
		// TODO return result of saving
		this.state = false;
	}

	/**
	 * Parse the given command, and apply the instructions on the artspace. The
	 * input may be a single instruction, a list of commands or a full human
	 * sentence, which is then converted in a correct list of instructions.
	 * 
	 * @param cmd
	 *            the command to read and analyse
	 */
	public void ParseCommand(String cmd) {
		// TODO parse a full sentence and convert it into known commands
		// for now, let's just assume each word (space separetad) is a command
		// TODO let "$param=value" et "#paramid=value" be special keywords
		// for example "$blur radius=3.14" ou "#12=42"
		AxLog.Debug("AkArtspace::ParseCommand =>> Parsing command \"" + cmd
				+ "\"");

		// check if list exists
		if (this.shapesInstructions == null) {
			this.shapesInstructions = new ArrayList<String>();
		}
		if (this.filtersInstructions == null) {
			this.filtersInstructions = new ArrayList<String>();
		}

		// add commands to instruction list
		String[] cmds = cmd.toLowerCase(Locale.ENGLISH).split(" ");
		for (int i = 0; i < cmds.length; i++) {
			if (this.shapeManager.Exists(cmds[i])) {
				this.shapesInstructions.add(cmds[i]);
				continue;
			}
			if (this.filterManager.Exists(cmds[i])) {
				this.filtersInstructions.add(cmds[i]);
				continue;
			}
		}

		// start redraw
		this.redraw();
	}

	/**
	 * Appends an instruction by adding the indexed command to the instruction
	 * list.
	 * 
	 * @param i
	 *            the index of the instruction to append
	 * @param type
	 *            the type of instruction (shape, filter, ...)
	 */
	public void AppendInstruction(int i, String type) {
		// check if list exists
		if (this.shapesInstructions == null) {
			this.shapesInstructions = new ArrayList<String>();
		}
		if (this.filtersInstructions == null) {
			this.filtersInstructions = new ArrayList<String>();
		}

		if (type.toLowerCase().equals("shape")) {
			if (this.shapeManager.Exists(i)) {
				this.shapesInstructions.add(this.shapeManager.GetName(i));
			}
		}

		if (type.toLowerCase().equals("filter")) {
			if (this.filterManager.Exists(i)) {
				this.filtersInstructions.add(this.filterManager.GetName(i));
			}
		}
		this.redraw();
	}

	/**
	 * Appends an instruction by adding the named command to the instruction
	 * list.
	 * 
	 * @param n
	 *            the name of the instruction to append
	 * @param type
	 *            the type of instruction (shape, filter, ...)
	 */
	public void AppendInstruction(String n, String type) {
		// check if list exists
		if (this.shapesInstructions == null) {
			this.shapesInstructions = new ArrayList<String>();
		}
		if (this.filtersInstructions == null) {
			this.filtersInstructions = new ArrayList<String>();
		}

		if (type.toLowerCase().equals("shape")) {
			if (this.shapeManager.Exists(n)) {
				this.shapesInstructions.add(n);
			}
		}

		if (type.toLowerCase().equals("filter")) {
			if (this.filterManager.Exists(n)) {
				this.filtersInstructions.add(n);
			}
		}

		this.redraw();
	}

	/**
	 * 
	 * @return boolean wether the artsapce has been modified since last save.
	 */
	public boolean IsUnsaved() {
		return this.state;
	}

	/**
	 * Resets the artspace.
	 * 
	 */
	public void Reset() {
		AxLog.Debug("AkArtspace::Reset =>> Reseting artspace");

		// create unique artspace ID;
		this.id = System.currentTimeMillis();

		this.state = false;
		this.shapesInstructions = null;
		this.filtersInstructions = null;
		this.paramsManager.ResetAllParameters();
		this.reset = true;
		this.redraw();
	}

	/**
	 * Get the current parameter manager.
	 * 
	 * @return the parameter manager
	 */
	public AxParameterManager GetParameterManager() {
		return this.paramsManager;
	}

	/**
	 * Get the current color manager.
	 * 
	 * @return the color manager
	 */
	public AkColorManager GetColorManager() {
		return this.colorManager;
	}

	/**
	 * Changes the fill and stroke color from the artspace color manager.
	 * 
	 */
	public void ChangeColors() {
		if (this.colorManager != null) {
			AxLog.Debug("AkArtspace::ChangeColors =>> Changing color");
			this.fill(this.colorManager.GetColor());
			this.stroke(this.colorManager.GetColor());
		} else {
			AxLog
					.Debug("AkArtspace::ChangeColors =>> No Color Manager, using random colors");
			this.fill(this.random(255), this.random(255), this.random(255));
			this.stroke(this.random(255), this.random(255), this.random(255));
		}
	}

	/**
	 * Select the brush to be used.
	 * 
	 * @param cmd
	 *            the name of the brush
	 */
	public void SelectBrush(String cmd) {
		if (this.brushManager.Exists(cmd)) {
			this.currentBrush = cmd;
			AxLog.Debug("AkArtspace::SelectBrush =>> now using brush \"" + cmd
					+ "\"");
		}
	}

	/**
	 * Select the blender to be used.
	 * 
	 * @param cmd
	 *            the name of the blender
	 */
	public void SelectBlender(String cmd) {
		if (this.blenderManager.Exists(cmd)) {
			this.currentBlend = cmd;
			AxLog.Debug("AkArtspace::SelectBlender =>> now using blender \""
					+ cmd + "\"");
		}
	}

	/**
	 * Draws a stroke using current brush between the given point.
	 * 
	 * @param srcx
	 *            the origin of the stroke (x value)
	 * @param srcy
	 *            the origin of the stroke (y value)
	 * @param destx
	 *            the destination of the stroke (x value)
	 * @param desty
	 *            the destination of the stroke (y value)
	 */
	public void DrawStroke(float srcx, float srcy, float destx, float desty) {
		AkBrush brush = (AkBrush) this.brushManager
				.GetElement(this.currentBrush);
		if (brush != null) {
			// AxLog.Debug("AkArtspace::DrawStroke =>> drawing stroke with \""
			// + brush.GetName() + "\" " + "(" + srcx + ";" + srcy + ") ("
			// + destx + ";" + desty + ")");
			brush.DrawStroke(this, srcx, srcy, destx, desty);
		}
	}

	/**
	 * Checks that a parameter exists, and if not create it.
	 * 
	 * @param name
	 *            the name of the parameter
	 * @param type
	 *            the type of the parameter
	 * @return boolean
	 */
	public boolean EnsureParameter(String name, String type) {
		if (this.paramsManager.Exists(name)) {
			AxParameter param = (AxParameter) this.paramsManager
					.GetElement(name);
			return (param.GetType().equals(type));
		} else {
			if (this.useGui) {
				String value = (String) JOptionPane.showInputDialog(this,
						"Set value for " + name + " (" + type + ")", TITLE
								+ " - Set parameter",
						JOptionPane.QUESTION_MESSAGE);

				this.paramsManager.AddParameter(type, name, value);
				return true;
			} else {
				this.paramsManager.AddParameter(type, name, "");
				return true;
			}
		}
	}

	/**
	 * Sets the selected parameter with a new String value
	 * 
	 * @param name
	 * @param value
	 */
	public void SetParameter(String name, String value) {
		if (this.paramsManager != null) {
			AxLog.Debug("AkArtspace::SetParameter =>> setting \"" + name
					+ "\" to \"" + value + "\"");
			if (this.paramsManager.Exists(name)) {
				this.paramsManager.GetParameter(name).SetValue(value);
			} else {
				AxLog.Debug("AkArtspace::SetParameter =>> parameter \"" + name
						+ "\" doesn't exist");
			}
		}
	}

	/** 
	 * 
	 */
	public void ApplyRandomInstruction() {
		// select the category
		int cat = AxRandom.RandI(2);
		
		switch (cat){
		case 0 : // shape 
			this.AppendInstruction(this.shapeManager.GetRandomElement().GetUID(),"shape");
		case 1 : // filter 
			this.AppendInstruction(this.filterManager.GetRandomElement().GetUID(),"filter");
		}
		
	}

}
